package hu.debrecen.adastra.card.service.impl;

import hu.debrecen.adastra.card.repository.domain.Account;
import hu.debrecen.adastra.card.service.AbstractGame;
import hu.debrecen.adastra.card.service.PlayerStatisticsService;
import hu.debrecen.adastra.card.service.PlayerStatisticsService.PlayerStatisticsEvent;
import hu.debrecen.adastra.card.service.domain.Card;
import hu.debrecen.adastra.card.service.domain.HungarianCard;
import hu.debrecen.adastra.card.service.domain.HungarianDeck;
import hu.debrecen.adastra.card.service.domain.PlayerState;
import hu.debrecen.adastra.card.service.domain.Round;
import hu.debrecen.adastra.card.web.domain.AbstractGameStateVO;
import hu.debrecen.adastra.card.web.domain.HandCardVO;
import hu.debrecen.adastra.card.web.domain.ZsirGameStateVO;

import java.util.LinkedList;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;

public class ZsirozasCardGame extends AbstractGame<HungarianCard> {

	private static final Logger LOGGER = LoggerFactory.getLogger(ZsirozasCardGame.class);

	private static final int CARD_IN_HAND = 4;

	public enum Action {
		OPPONENT_CALLED, CALLED, HIT, LOST, WAITING
	}

	public ZsirozasCardGame(final String gameId, final Account owner, final PlayerStatisticsService playerStatisticsService) {
		this(gameId, new HungarianDeck(), owner, playerStatisticsService);
	}

	public ZsirozasCardGame(final String gameId, final HungarianDeck deck, final Account owner, final PlayerStatisticsService playerStatisticsService) {
		super(gameId, owner, deck, 2, playerStatisticsService);
	}

	@Override
	public void call(final Account player, final String type, final String suit) {

		Round round = this.getLastRound();
		Preconditions.checkState(round.getActualPlayer().equals(player), "You cannot call!");

		Card card = this.findCardInPlayerHand(player, type, suit);

		ZsirozasCardGame.LOGGER.debug("Player({}) calls a card({}) in {}. round", new Object[] { player.getUsername(), card, this.getRoundCount() });

		this.putCardOnTheTable(player, card);

		PlayerStatisticsEvent statistics = this.playerStatisticsService.start(player);

		statistics.increaseCallCount();

		Account opponent = round.getNextPlayer();
		// Start new round
		if (round.isFirstCard()) {
			ZsirozasCardGame.LOGGER.debug("First card({}) on the table. Player({})", card, player);
			round.setWinner(player);
			this.setStoreAction(player, Action.CALLED);
			this.setStoreAction(opponent, Action.OPPONENT_CALLED);
		} else
		// Call and Hit
		if (!round.isFirstCard() && this.isHit(round.getFirstCard(), card)) {
			ZsirozasCardGame.LOGGER.debug("Player({}) hits the first card({}) with card({}).", new Object[] { player, round.getFirstCard(), card });
			round.setWinner(player);

			if (this.canHit(opponent, round.getFirstCard())) {
				ZsirozasCardGame.LOGGER.debug("Player({}) is able to hit.", opponent);
				this.setStoreAction(player, Action.CALLED);
				this.setStoreAction(opponent, Action.OPPONENT_CALLED);
			} else if ((round.getCards().size() % 2) == 1) {
				ZsirozasCardGame.LOGGER.debug("Player({}) is not able to hit. But he/she has to call.", opponent);
				this.setStoreAction(player, Action.CALLED);
				this.setStoreAction(opponent, Action.OPPONENT_CALLED);
			} else

			// If opponent is not able to hit and number of card on table is
			// even
			if (!this.canHit(opponent, round.getFirstCard()) && ((round.getCards().size() % 2) == 0)) {
				this.setStoreAction(player, Action.CALLED, Action.HIT);
				this.setStoreAction(opponent, Action.OPPONENT_CALLED, Action.LOST);
				this.endRoundAction(round);
			}

		} else
		// Call and Lost
		if (!round.isFirstCard() && !this.isHit(round.getFirstCard(), card)) {
			this.setStoreAction(opponent, Action.OPPONENT_CALLED, Action.HIT);
			this.setStoreAction(player, Action.CALLED, Action.LOST);
			this.endRoundAction(round);
		}

		statistics.save();

		round.nextPlayer();

		this.updateLastActionTime(opponent);
	}

	@Override
	public AbstractGameStateVO getGameState(final Account player) {
		return this.getGameActionState(player, new Action[0]);
	}

	private AbstractGameStateVO getGameActionState(final Account player, final Action... actions) {
		Round round = this.getLastRound();
		Account opponentPlayer = round.getNextPlayer(player);
		ZsirozasCardGame.LOGGER.trace("Player:{}, opponent player:{}", player, opponentPlayer);
		PlayerState playerState = this.getPlayerState(player);
		PlayerState opponentState = this.getPlayerState(opponentPlayer);
		ZsirozasCardGame.LOGGER.trace("player hand size: {}, opponent hand size: {}", new Object[] { playerState.getHand().size(), opponentState.getHand().size() });

		boolean caller = (actions.length == 0) && player.equals(round.getActualPlayer());

		List<Card> table = round.getCards();
		boolean possiblePass = (table.size() != 0) && ((table.size() % 2) == 0) && caller && this.canHit(player, round.getFirstCard());

		List<HandCardVO> hand = possiblePass ? this.getValidCard(playerState.getHand(), round.getFirstCard()) : this.getAllValidCard(playerState.getHand());
		boolean timeExpWarning = caller ? this.isExpiredByLastAction() : false;

		return new ZsirGameStateVO(this.getDeck().size(), caller, possiblePass, timeExpWarning, table, hand, opponentState.getHand(), actions);
	}

	private List<HandCardVO> getValidCard(final List<Card> hand, final Card firstCard) {
		return Lists.newLinkedList(Collections2.transform(hand, new Function<Card, HandCardVO>() {

			@Override
			public HandCardVO apply(final Card card) {
				return new HandCardVO(card, ZsirozasCardGame.this.isHit(firstCard, card));
			}

		}));
	}

	public void pass(final Account player) {
		Round round = this.getLastRound();

		Preconditions.checkState(round.getActualPlayer().equals(player), "You cannot say pass!");

		ZsirozasCardGame.LOGGER.debug("Player({}) says pass.", player);
		this.setStoreAction(player, Action.LOST);
		this.setStoreAction(round.getNextPlayer(), Action.HIT);

		this.endRoundAction(round);
	}

	private void setStoreAction(final Account player, final Action... actions) {
		ZsirozasCardGame.LOGGER.debug("Put gameState for player({}), action({})", player, actions);
		this.actionGameState.put(player, this.getGameActionState(player, actions));
	}

	private void endRoundAction(final Round round) {
		this.endRound(round.getWinner(), round.getCards());
		this.startNewRound(round.getWinner());
	}

	@Override
	protected void startNewRound(final Account player) {
		super.startNewRound(player);

		for (Account p : this.getAccounts()) {
			ZsirozasCardGame.LOGGER.trace("Hand of player({}): size: {}", p, this.getPlayerState(p).getHand().size());
		}

		int count = ZsirozasCardGame.CARD_IN_HAND - this.getPlayerState(player).getHand().size();
		for (int i = 0; (i < count) && !this.getDeck().isEmpty(); i++) {
			for (Account p : this.getAccounts()) {
				Card nextCard = this.getDeck().nextCard();
				this.getPlayerState(p).getHand().add(nextCard);
				ZsirozasCardGame.LOGGER.debug("Player({}) pull a new card({})", p, nextCard);
			}
		}

		for (Account p : this.getAccounts()) {
			if (!this.getDeck().isEmpty() && (this.getPlayerState(p).getHand().size() != ZsirozasCardGame.CARD_IN_HAND)) {
				throw new IllegalStateException("Number of the card in hand of player is not equal with " + ZsirozasCardGame.CARD_IN_HAND);
			}
		}

	}

	@Override
	protected PlayerState getDefaultPlayerState() {

		List<Card> cards = new LinkedList<Card>();
		for (int i = 0; i < ZsirozasCardGame.CARD_IN_HAND; i++) {
			cards.add(this.getDeck().nextCard());
		}

		return new PlayerState(cards);
	}

	protected boolean canHit(final Account actualPlayer, final Card firstCard) {

		List<Card> hand = this.getPlayerState(actualPlayer).getHand();
		ZsirozasCardGame.LOGGER.trace("Player({}) cards:{}", actualPlayer, hand);

		return Iterables.any(hand, new Predicate<Card>() {
			@Override
			public boolean apply(final Card card) {
				return ZsirozasCardGame.this.isHit(firstCard, card);
			}
		});

	}

	protected boolean isHit(final Card firstCard, final Card card) {
		Preconditions.checkState(!firstCard.equals(card), "Called card is a same as a first card.");
		return firstCard.getType().equals(card.getType()) || card.getType().equals(HungarianCard.Type.VII.name());
	}

}
